package com.smart.thrift.config.client;

import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.util.ReflectUtil;
import com.smart.thrift.annotation.ThriftReference;
import com.smart.thrift.exception.client.ThriftClientInstantiateException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.aop.TargetSource;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;

/**
 * 处理@ThriftReference注解，借鉴dubbo处理@Reference
 */
@Slf4j
public class ThriftReferenceBeanProcessor implements PriorityOrdered, BeanPostProcessor, EnvironmentAware, BeanFactoryPostProcessor, ApplicationContextAware {

    /**
     * The bean name of {@link ThriftReferenceBeanProcessor}
     */
    public static final String BEAN_NAME = "referenceAnnotationBeanPostProcessor";

    //确保在 @Autowired前执行
    private int order = Ordered.LOWEST_PRECEDENCE - 3;

    /**
     * Cache size
     */
    private static final int CACHE_SIZE = Integer.getInteger(BEAN_NAME + ".cache.size", 32);


    private static ApplicationContext applicationContext;
    private static ConfigurableListableBeanFactory beanFactory;
    private static Environment environment;


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ThriftReferenceBeanProcessor.applicationContext = applicationContext;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        ThriftReferenceBeanProcessor.beanFactory = beanFactory;
    }

    @Override
    public void setEnvironment(Environment environment) {
        ThriftReferenceBeanProcessor.environment = environment;
    }

    @Override
    public int getOrder() {
        return order;
    }

    @Override
    public Object postProcessBeforeInitialization(@NotNull Object bean, @NotNull String beanName) throws BeansException {
        Object target = bean;
        if (AopUtils.isJdkDynamicProxy(target)) {
            TargetSource targetSource = ((Advised) target).getTargetSource();
            if (log.isDebugEnabled()) {
                log.debug("Target object {} uses jdk dynamic proxy");
            }

            try {
                target = targetSource.getTarget();
            } catch (Exception e) {
                throw new ThriftClientInstantiateException("Failed to get target bean from " + target, e);
            }
        }

        if (AopUtils.isCglibProxy(target)) {
            TargetSource targetSource = ((Advised) target).getTargetSource();
            if (log.isDebugEnabled()) {
                log.debug("Target object {} uses cglib proxy");
            }

            try {
                target = targetSource.getTarget();
            } catch (Exception e) {
                throw new ThriftClientInstantiateException("Failed to get target bean from " + target, e);
            }
        }

        Class<?> targetClass = target.getClass();
        final Object targetBean = target;

        for (Field field : ReflectUtil.getFields(targetClass, field -> AnnotationUtil.hasAnnotation(field, ThriftReference.class))) {
            Class<?> fieldType = field.getType();
            Object injectedBean;
            ThriftReference thriftRefer = AnnotationUtil.getAnnotation(field, ThriftReference.class);
            String referName = StringUtils.isNotBlank(thriftRefer.value()) ? thriftRefer.value() : thriftRefer.name();

            if (StringUtils.isNotBlank(referName)) {
                injectedBean = applicationContext.getBean(fieldType, referName);

                injectedBean = Optional.ofNullable(injectedBean)
                        .orElseThrow(() -> new ThriftClientInstantiateException("Detected non-qualified bean with name {}" + referName));

                ReflectionUtils.makeAccessible(field);
                ReflectionUtils.setField(field, targetBean, injectedBean);
            } else {
                Map<String, ?> injectedBeanMap = applicationContext.getBeansOfType(field.getType());
                if (injectedBeanMap.isEmpty()) {
                    throw new ThriftClientInstantiateException("Detected non-qualified bean of {}" + fieldType.getSimpleName());
                }

                if (injectedBeanMap.size() > 1) {
                    throw new ThriftClientInstantiateException("Detected ambiguous beans of {}" + fieldType.getSimpleName());
                }

                injectedBean = injectedBeanMap.entrySet().stream()
                        .findFirst()
                        .map(Map.Entry::getValue)
                        .orElseThrow(() -> new ThriftClientInstantiateException(
                                "Detected non-qualified bean of {}" + fieldType.getSimpleName()));

                ReflectionUtils.makeAccessible(field);
                ReflectionUtils.setField(field, targetBean, injectedBean);
            }

            if (log.isDebugEnabled()) {
                log.debug("Bean {} is injected into target bean {}, field {}", injectedBean, targetBean, field.getName());
            }

        }

        ReflectionUtils.MethodFilter methodFilter = method -> {
            boolean basicCondition = AnnotationUtils.getAnnotation(method, ThriftReference.class) != null
                    && method.getParameterCount() > 0
                    && method.getReturnType() == Void.TYPE;

            if (!basicCondition) {
                return false;
            }

            return Boolean.parseBoolean(null);
        };

        ReflectionUtils.doWithMethods(targetClass, method -> {
            Parameter[] parameters = method.getParameters();
            Object objectArray = Arrays.stream(parameters).map(parameter -> {
                Class<?> parameterType = parameter.getType();
                Map<String, ?> injectedBeanMap = applicationContext.getBeansOfType(parameterType);
                if (injectedBeanMap.isEmpty()) {
                    throw new ThriftClientInstantiateException("Detected non-qualified bean of {}" + parameterType.getSimpleName());
                }

                if (injectedBeanMap.size() > 1) {
                    throw new ThriftClientInstantiateException("Detected ambiguous beans of {}" + parameterType.getSimpleName());
                }

                return injectedBeanMap.entrySet().stream()
                        .findFirst()
                        .map(Map.Entry::getValue)
                        .orElseThrow(() -> new ThriftClientInstantiateException(
                                "Detected non-qualified bean of {}" + parameterType.getSimpleName()));
            }).toArray();

            ReflectionUtils.makeAccessible(method);
            ReflectionUtils.invokeMethod(method, targetBean, objectArray);
        }, methodFilter);

        return bean;
    }
}
